react-linear-feedback 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -36
- package/dist/embed/index.d.ts +48 -0
- package/dist/embed/index.js +2695 -0
- package/dist/embed/linear-feedback.js +127 -0
- package/dist/react/index.cjs +37 -15
- package/dist/react/index.d.cts +10 -2
- package/dist/react/index.d.ts +10 -2
- package/dist/react/index.js +37 -15
- package/dist/server/index.cjs +96 -50
- package/dist/server/index.d.cts +37 -9
- package/dist/server/index.d.ts +37 -9
- package/dist/server/index.js +94 -50
- package/dist/vite/index.cjs +212 -0
- package/dist/vite/index.d.cts +45 -0
- package/dist/vite/index.d.ts +45 -0
- package/dist/vite/index.js +210 -0
- package/package.json +30 -10
package/README.md
CHANGED
|
@@ -2,34 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/react-linear-feedback) [](./LICENSE)
|
|
4
4
|
|
|
5
|
-
**A drop-in
|
|
5
|
+
**A drop-in feedback widget that turns a drawn box + note into a Linear issue with an annotated screenshot.** Use it as a React component, or embed it on **any site** — Framer, Webflow, plain HTML — with a single script tag.
|
|
6
6
|
|
|
7
|
-
A
|
|
7
|
+
A user opens the widget, **drags a box** over the page, picks a type (**Bug / Improvement**), writes a note — and it captures a screenshot and files a Linear issue. Self-contained styling, no design system required.
|
|
8
8
|
|
|
9
9
|
<!-- Demo: record a short GIF and drop it here →  -->
|
|
10
10
|
> _Draw a box anywhere → type a note → it lands in Linear with the annotated screenshot and page context._
|
|
11
11
|
|
|
12
|
+
- 🌍 **Three ways in** — a React component, a framework-agnostic `init()` import for any bundler app, or a ~23KB script tag for sites with no build step
|
|
12
13
|
- 🪶 **Themeable in one prop** — no CSS import, no Tailwind, no design system; styles are injected at runtime
|
|
13
|
-
- 🌍 **Works in any React app** — Next.js, Vite, Remix, CRA… (no `next` dependency; `"use client"` is built in)
|
|
14
14
|
- 🖼️ **Annotated screenshots** via [`modern-screenshot`](https://github.com/qq15725/modern-screenshot) (handles Tailwind v4 / `oklch()`)
|
|
15
|
-
- 🏷️ **Labels by name, self-healing** — resolved at request time, so recoloring/recreating a label in Linear won't break it; applied best-effort
|
|
16
|
-
- 🔌 **
|
|
15
|
+
- 🏷️ **Labels by name, self-healing** — resolved at request time (and cached), so recoloring/recreating a label in Linear won't break it; applied best-effort
|
|
16
|
+
- 🔌 **One Web-standard server handler** + Next.js, Node/Express, and Vite-dev adapters, with built-in CORS for cross-origin embeds
|
|
17
17
|
|
|
18
18
|
## Contents
|
|
19
19
|
|
|
20
|
-
- [
|
|
20
|
+
- [How it fits together](#how-it-fits-together) · [React apps (npm)](#react-apps-npm) · [Any framework (npm import)](#any-framework-npm-import) · [Any site (script tag)](#any-site-script-tag) · [The server handler](#the-server-handler) · [Linear setup](#linear-setup) · [Configuration](#configuration) · [Theming](#theming) · [Custom types](#custom-types) · [Security](#security) · [Troubleshooting](#troubleshooting) · [License](#license)
|
|
21
21
|
|
|
22
|
-
##
|
|
22
|
+
## How it fits together
|
|
23
|
+
|
|
24
|
+
The widget (React component or script-tag embed) never talks to Linear directly — it POSTs to **your** endpoint, which holds the API key server-side and creates the issue:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
widget (browser) ──POST──▶ your endpoint (createWebHandler) ──▶ Linear API
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
So every setup is two steps: mount the widget, and deploy the handler ([The server handler](#the-server-handler)).
|
|
31
|
+
|
|
32
|
+
## React apps (npm)
|
|
23
33
|
|
|
24
34
|
```bash
|
|
25
35
|
npm i react-linear-feedback
|
|
26
|
-
# the server entry needs the Linear SDK (optional peer):
|
|
27
|
-
npm i @linear/sdk
|
|
28
36
|
```
|
|
29
37
|
|
|
30
|
-
`react` / `react-dom` are peer dependencies. `@linear/sdk`
|
|
38
|
+
`react` / `react-dom` are peer dependencies. `@linear/sdk` ships as a dependency — it's imported only by the server entry, so it's tree-shaken out of client bundles.
|
|
31
39
|
|
|
32
|
-
|
|
40
|
+
### Next.js (App Router)
|
|
33
41
|
|
|
34
42
|
**1. Server route** — `app/api/feedback/route.ts`:
|
|
35
43
|
|
|
@@ -62,38 +70,169 @@ export default function RootLayout({ children }) {
|
|
|
62
70
|
|
|
63
71
|
No `"use client"` needed — the package ships it, so you can mount `<FeedbackGate>` straight from a Server Component layout.
|
|
64
72
|
|
|
65
|
-
**3. Turn it on.**
|
|
73
|
+
**3. Turn it on.** `<FeedbackGate>` is **hidden by default**. Visit any page with **`?feedback`** (or `?feedback=1`) to enable it; a cookie remembers the choice for 90 days. `?feedback=0` turns it off. (Use `<FeedbackWidget>` instead for an always-visible button.)
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
### Vite SPA
|
|
68
76
|
|
|
69
|
-
Mount the widget and
|
|
77
|
+
Mount the widget the same way; the handler runs as a serverless function in production and as a dev-server plugin locally:
|
|
70
78
|
|
|
71
79
|
```tsx
|
|
72
80
|
import { FeedbackGate } from "react-linear-feedback/react";
|
|
73
81
|
|
|
74
|
-
<FeedbackGate
|
|
82
|
+
<FeedbackGate brandColor="#7f56d9" />; // endpoint defaults to /api/feedback (same origin)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Production — a Vercel serverless function** at `api/feedback.ts`:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { createNodeHandler, cookieGate } from "react-linear-feedback/server";
|
|
89
|
+
|
|
90
|
+
export default createNodeHandler({
|
|
91
|
+
apiKey: process.env.LINEAR_API_KEY!,
|
|
92
|
+
teamId: process.env.LINEAR_TEAM_ID!,
|
|
93
|
+
authorize: cookieGate("wh_feedback"),
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Set `LINEAR_API_KEY` / `LINEAR_TEAM_ID` in your Vercel project's env (server-side — **not** `VITE_`-prefixed, so they never reach the bundle).
|
|
98
|
+
|
|
99
|
+
**Local dev — the Vite plugin**, so `vite dev` serves the same endpoint (without it, `POST /api/feedback` 404s locally):
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// vite.config.ts
|
|
103
|
+
import { defineConfig, loadEnv } from "vite";
|
|
104
|
+
import { linearFeedback } from "react-linear-feedback/vite";
|
|
105
|
+
import { cookieGate } from "react-linear-feedback/server";
|
|
106
|
+
|
|
107
|
+
export default defineConfig(({ mode }) => {
|
|
108
|
+
const env = loadEnv(mode, process.cwd(), ""); // reads .env (LINEAR_* are server-side, un-prefixed)
|
|
109
|
+
return {
|
|
110
|
+
plugins: [
|
|
111
|
+
linearFeedback({
|
|
112
|
+
apiKey: env.LINEAR_API_KEY,
|
|
113
|
+
teamId: env.LINEAR_TEAM_ID,
|
|
114
|
+
authorize: cookieGate("wh_feedback"),
|
|
115
|
+
}),
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The plugin is dev-only (`apply: "serve"`) — it has no effect on the production build.
|
|
122
|
+
|
|
123
|
+
## Any framework (npm import)
|
|
124
|
+
|
|
125
|
+
Not a React app? The same self-mounting widget is available as a plain ESM import — Vue, Svelte, Astro, Solid, vanilla TS, anything with a bundler. **Zero peer dependencies** (a compact React-compatible runtime is bundled inside, ~23KB gz):
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { init, destroy } from "react-linear-feedback/embed";
|
|
129
|
+
|
|
130
|
+
init({
|
|
131
|
+
endpoint: "/api/feedback",
|
|
132
|
+
brandColor: "#7f56d9",
|
|
133
|
+
// mode: "gated", // ?feedback URL-param gate, like <FeedbackGate>
|
|
134
|
+
// token: "...", // for headerGate'd cross-origin endpoints
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// destroy() unmounts it (e.g. SPA route teardown / HMR)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`init()` takes the same options as the React component plus `mode` and `token` (see [Configuration](#configuration)). Calling it again replaces the existing instance.
|
|
141
|
+
|
|
142
|
+
React apps should prefer the [`<FeedbackGate>` component](#react-apps-npm) — it shares the page's React instead of shipping a second renderer.
|
|
143
|
+
|
|
144
|
+
## Any site (script tag)
|
|
145
|
+
|
|
146
|
+
No npm, no build step, no React on the page — the embed bundle (~23KB gz, served from a CDN off the npm package) mounts the same widget on any site: Framer, Webflow, WordPress, plain HTML.
|
|
147
|
+
|
|
148
|
+
```html
|
|
149
|
+
<script
|
|
150
|
+
src="https://cdn.jsdelivr.net/npm/react-linear-feedback@0.3.0/dist/embed/linear-feedback.js"
|
|
151
|
+
data-endpoint="https://your-app.example.com/api/feedback"
|
|
152
|
+
data-brand-color="#7f56d9"
|
|
153
|
+
data-token="your-embed-token"
|
|
154
|
+
defer
|
|
155
|
+
></script>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Pin the version in the URL (as above). Because the page and the endpoint are usually on **different origins**, the handler needs `allowedOrigins` set — see [The server handler](#the-server-handler).
|
|
159
|
+
|
|
160
|
+
It must be a classic `<script src>` tag (not `type="module"`, not injected via `innerHTML`) so the embed can read its own config; if you need dynamic injection, use `data-manual` + `init()` below.
|
|
161
|
+
|
|
162
|
+
### Script-tag options
|
|
163
|
+
|
|
164
|
+
| Attribute | Maps to | Notes |
|
|
165
|
+
| --- | --- | --- |
|
|
166
|
+
| `data-endpoint` | `endpoint` | **Required in practice** — your deployed handler URL |
|
|
167
|
+
| `data-brand-color` | `brandColor` | Any CSS color |
|
|
168
|
+
| `data-position` | `position` | `bottom-right` (default) \| `bottom-left` \| `top-right` \| `top-left` \| `right` \| `left` |
|
|
169
|
+
| `data-fab-label` | `fabLabel` | Button text |
|
|
170
|
+
| `data-mode` | `mode` | `open` (default, always visible) \| `gated` (`?feedback` URL param + cookie, like `<FeedbackGate>`) |
|
|
171
|
+
| `data-token` | `token` | Sent as the `x-feedback-token` header — pair with `headerGate` on the server |
|
|
172
|
+
| `data-z-index` | `zIndex` | Override the widget's stacking context |
|
|
173
|
+
| `data-name-required` | `nameRequired` | `"false"` to skip the name prompt |
|
|
174
|
+
| `data-manual` | — | `"true"` disables auto-init; call `LinearFeedback.init()` yourself |
|
|
175
|
+
|
|
176
|
+
### JS API
|
|
177
|
+
|
|
178
|
+
Options that aren't expressible as data attributes (like custom `types`) go through `window.lfbConfig` (set **before** the script tag; merged over the data attributes) or the imperative API:
|
|
179
|
+
|
|
180
|
+
```html
|
|
181
|
+
<script>
|
|
182
|
+
window.lfbConfig = {
|
|
183
|
+
types: [
|
|
184
|
+
{ id: "bug", label: "Bug", color: "#ef4444", icon: "bug" },
|
|
185
|
+
{ id: "idea", label: "Idea", color: "#22c55e", icon: "improvement" },
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
</script>
|
|
189
|
+
<script src="https://cdn.jsdelivr.net/npm/react-linear-feedback@0.3.0/dist/embed/linear-feedback.js" data-endpoint="..." defer></script>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
// With data-manual="true":
|
|
194
|
+
LinearFeedback.init({ endpoint: "https://...", brandColor: "#7f56d9", token: "..." });
|
|
195
|
+
LinearFeedback.destroy();
|
|
75
196
|
```
|
|
76
197
|
|
|
77
|
-
|
|
198
|
+
A runnable demo lives in [`examples/embed.html`](./examples/embed.html) + [`examples/dev-server.mjs`](./examples/dev-server.mjs).
|
|
199
|
+
|
|
200
|
+
## The server handler
|
|
201
|
+
|
|
202
|
+
`createWebHandler` is the canonical handler — a Web-standard `(Request) => Promise<Response>` function. `createNextRoute` is the same function under its Next.js name, and `createNodeHandler` wraps it for `(req, res)`-style servers (Express, plain `node:http`, Vercel Node functions). All of them run on the **Node runtime** (the screenshot upload uses `Buffer`; Edge isn't supported).
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { createWebHandler, headerGate } from "react-linear-feedback/server";
|
|
206
|
+
|
|
207
|
+
const handler = createWebHandler({
|
|
208
|
+
apiKey: process.env.LINEAR_API_KEY!,
|
|
209
|
+
teamId: process.env.LINEAR_TEAM_ID!,
|
|
210
|
+
// Required for script-tag embeds on other domains — enables CORS (preflight + headers):
|
|
211
|
+
allowedOrigins: ["https://your-marketing-site.com", "https://www.your-marketing-site.com"],
|
|
212
|
+
// Cookie gates can't cross origins — gate embeds with the shared token instead:
|
|
213
|
+
authorize: headerGate("your-embed-token"),
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Cross-origin example end-to-end: the script tag on `your-marketing-site.com` has `data-endpoint="https://your-app.example.com/api/feedback"` and `data-token="your-embed-token"`; the handler at that endpoint lists the marketing origin in `allowedOrigins` and gates with `headerGate("your-embed-token")`. Same-origin setups (widget and handler on one domain) need neither.
|
|
218
|
+
|
|
219
|
+
### Express / other Node servers
|
|
220
|
+
|
|
221
|
+
`createNodeHandler` works with or without `express.json()`:
|
|
78
222
|
|
|
79
223
|
```ts
|
|
80
224
|
import express from "express";
|
|
81
|
-
import cors from "cors";
|
|
82
225
|
import { createNodeHandler, cookieGate } from "react-linear-feedback/server";
|
|
83
226
|
|
|
84
227
|
const app = express();
|
|
85
|
-
|
|
86
|
-
app.use("/feedback", cors({ origin: "https://your-site.com", credentials: true }));
|
|
87
|
-
app.post("/feedback", createNodeHandler({
|
|
228
|
+
app.post("/api/feedback", createNodeHandler({
|
|
88
229
|
apiKey: process.env.LINEAR_API_KEY!,
|
|
89
230
|
teamId: process.env.LINEAR_TEAM_ID!,
|
|
90
|
-
authorize: cookieGate("wh_feedback"),
|
|
231
|
+
authorize: cookieGate("wh_feedback"),
|
|
91
232
|
}));
|
|
92
233
|
app.listen(8787);
|
|
93
234
|
```
|
|
94
235
|
|
|
95
|
-
The handler reads the raw body itself, so it works with or without `express.json()`.
|
|
96
|
-
|
|
97
236
|
## Linear setup
|
|
98
237
|
|
|
99
238
|
You need three things from Linear:
|
|
@@ -110,7 +249,7 @@ LINEAR_TEAM_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
|
110
249
|
|
|
111
250
|
## Configuration
|
|
112
251
|
|
|
113
|
-
### Widget props (`<FeedbackGate>` / `<FeedbackWidget>`)
|
|
252
|
+
### Widget props (`<FeedbackGate>` / `<FeedbackWidget>` / `LinearFeedback.init`)
|
|
114
253
|
|
|
115
254
|
Most apps only set `brandColor` and maybe `endpoint`.
|
|
116
255
|
|
|
@@ -118,32 +257,35 @@ Most apps only set `brandColor` and maybe `endpoint`.
|
|
|
118
257
|
| --- | --- | --- |
|
|
119
258
|
| `endpoint` | `/api/feedback` | Where the widget POSTs |
|
|
120
259
|
| `brandColor` | `#6366f1` | FAB / active / focus color (sets the `--lfb-brand` CSS var) |
|
|
121
|
-
| `position` | `bottom-right` | `bottom-right` \| `bottom-left` \| `top-right` \| `top-left` |
|
|
260
|
+
| `position` | `bottom-right` | `bottom-right` \| `bottom-left` \| `top-right` \| `top-left` \| `right` \| `left` (edge tabs: compact icon-only launcher) |
|
|
122
261
|
| `types` | Bug, Improvement | `{ id, label, color, icon? }[]` shown in the composer |
|
|
123
262
|
| `nameRequired` | `true` | Ask for a reporter name before the first submission (saved to localStorage, included in the issue) |
|
|
124
263
|
| `nameStorageKey` | `wh_feedback_name` | localStorage key for the remembered name |
|
|
125
264
|
| `fabLabel` | `Give feedback` | Floating button text |
|
|
265
|
+
| `zIndex` | `2147483640` | Stacking context for the widget's layers (sugar for `--lfb-z`, see [Theming](#theming)) |
|
|
266
|
+
| `requestHeaders` | — | Extra headers sent with the submission (the embed's `token` uses this) |
|
|
126
267
|
| `urlParam` ¹ | `feedback` | Toggle param |
|
|
127
268
|
| `cookieName` ¹ | `wh_feedback` | Enabled-state cookie name |
|
|
128
269
|
| `cookieValue` ¹ | `1` | Cookie value when enabled |
|
|
129
270
|
| `cookieMaxAgeSeconds` ¹ | `7776000` (90d) | Cookie lifetime |
|
|
130
271
|
|
|
131
|
-
¹ `<FeedbackGate>` only.
|
|
272
|
+
¹ `<FeedbackGate>` only (and the embed's `mode: "gated"`).
|
|
132
273
|
|
|
133
|
-
### Server config (`createNextRoute` / `createNodeHandler` / `createFeedbackIssue`)
|
|
274
|
+
### Server config (`createWebHandler` / `createNextRoute` / `createNodeHandler` / `createFeedbackIssue`)
|
|
134
275
|
|
|
135
276
|
| Field | Description |
|
|
136
277
|
| --- | --- |
|
|
137
278
|
| `apiKey` | Linear personal API key (**server-side only**) |
|
|
138
279
|
| `teamId` | Target team UUID |
|
|
139
280
|
| `labels` | Optional `{ [typeId]: labelName }` map. Default: the type id **is** the label name (so type `bug` → label `bug`) |
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
281
|
+
| `labelCacheTtlMs` | How long label name→ID lookups are cached (default 10 min; `0` disables). Saves a Linear API round-trip per submission |
|
|
282
|
+
| `allowedOrigins` | Origins allowed to call the endpoint cross-origin (exact origins or `"*"`); enables CORS. Required for script-tag embeds on other domains |
|
|
283
|
+
| `authorize(req)` | Optional gate; return `false` to reject. `cookieGate(name, value="1")` and `headerGate(token, headerName="x-feedback-token")` are provided |
|
|
142
284
|
|
|
143
285
|
Remap type ids to differently-named Linear labels:
|
|
144
286
|
|
|
145
287
|
```ts
|
|
146
|
-
|
|
288
|
+
createWebHandler({
|
|
147
289
|
apiKey, teamId,
|
|
148
290
|
labels: { bug: "bug", idea: "feature-request" },
|
|
149
291
|
});
|
|
@@ -151,9 +293,15 @@ createNextRoute({
|
|
|
151
293
|
|
|
152
294
|
## Theming
|
|
153
295
|
|
|
154
|
-
Set `brandColor`, or override any CSS variable on `.lfb-
|
|
296
|
+
Set `brandColor`, or override any CSS variable on `.lfb-root`:
|
|
155
297
|
`--lfb-brand`, `--lfb-fg`, `--lfb-surface`, `--lfb-border`, `--lfb-radius`, `--lfb-rect`, `--lfb-z`, `--lfb-font`.
|
|
156
298
|
|
|
299
|
+
The widget defaults to a very high `--lfb-z` (`2147483640`) so it sits above app chrome. If it lands on top of your own modals or toasts, lower it — either with the `zIndex` prop / `data-z-index` attribute, or in CSS:
|
|
300
|
+
|
|
301
|
+
```css
|
|
302
|
+
.lfb-root { --lfb-z: 40; }
|
|
303
|
+
```
|
|
304
|
+
|
|
157
305
|
## Custom types
|
|
158
306
|
|
|
159
307
|
```tsx
|
|
@@ -165,25 +313,31 @@ Set `brandColor`, or override any CSS variable on `.lfb-doc-layer, .lfb-fixed-la
|
|
|
165
313
|
/>
|
|
166
314
|
```
|
|
167
315
|
|
|
168
|
-
Each `type.id` is matched to a Linear label of the same name. If your label is named differently, map it on the server via `labels` (above). Built-in `icon` values: `"bug"`, `"improvement"`, `"dot"`.
|
|
316
|
+
Each `type.id` is matched to a Linear label of the same name. If your label is named differently, map it on the server via `labels` (above). Built-in `icon` values: `"bug"`, `"improvement"`, `"dot"`. For the script-tag embed, pass `types` via `window.lfbConfig` or `LinearFeedback.init()`.
|
|
169
317
|
|
|
170
318
|
## Security
|
|
171
319
|
|
|
172
320
|
⚠️ **The endpoint creates Linear issues**, so it's effectively write access to your tracker, and it's **open by default**. Before shipping on a public page:
|
|
173
321
|
|
|
174
|
-
- **Gate it** with `authorize` (e.g. `cookieGate`, or your own session check).
|
|
175
|
-
- **Restrict
|
|
322
|
+
- **Gate it** with `authorize` (e.g. `cookieGate`, `headerGate`, or your own session check).
|
|
323
|
+
- **Restrict origins** with `allowedOrigins: ["https://your-site.com"]`.
|
|
176
324
|
- **Rate-limit** if the page is public (e.g. per-IP).
|
|
177
325
|
|
|
326
|
+
### Gating an embedded (cross-origin) widget
|
|
327
|
+
|
|
328
|
+
`cookieGate` only works **same-origin**: the gate cookie is written on the page's origin, so the browser never sends it with a cross-origin submission. For script-tag embeds, use the shared token instead — `data-token="..."` on the script tag plus `authorize: headerGate("...")` on the handler. The token is visible in the page source, so treat it as a **tripwire against drive-by spam, not authentication**; combine it with `allowedOrigins` and rate limiting.
|
|
329
|
+
|
|
178
330
|
Your `LINEAR_API_KEY` stays server-side; the package never ships it to the browser. Screenshots are uploaded to Linear's **private** asset storage — they render inside the issue, but the URL needs a fresh signed token to fetch elsewhere (it can't be hot-linked). If a screenshot upload fails, the issue is still created without it.
|
|
179
331
|
|
|
180
332
|
## Troubleshooting
|
|
181
333
|
|
|
182
334
|
Submissions never throw in the UI — failures are logged to the browser console with a `[feedback]` prefix, and server errors return a JSON `message`. If issues aren't being created, check:
|
|
183
335
|
|
|
184
|
-
- `endpoint` points at your route, and `LINEAR_API_KEY` / `LINEAR_TEAM_ID` are set.
|
|
185
|
-
- **
|
|
336
|
+
- `endpoint` points at your route, and `LINEAR_API_KEY` / `LINEAR_TEAM_ID` are set (the handler logs a `[feedback]` warning at startup when they're missing).
|
|
337
|
+
- A rejected `authorize` returns **404 by design** — the endpoint is hidden, not just forbidden. If you're debugging your gate and seeing 404s, it's not a routing problem.
|
|
338
|
+
- **CORS**: cross-origin pages (script-tag embeds) need their origin in `allowedOrigins`, and the browser must reach the endpoint with both `OPTIONS` and `POST`.
|
|
186
339
|
- `runtime = "nodejs"` is set on the Next.js route (Edge has no `Buffer`).
|
|
340
|
+
- On a **Vite SPA**, `POST /api/feedback` 404s under `vite dev` unless you add the [`linearFeedback` Vite plugin](#vite-spa) (or run `vercel dev`). In production it's served by your deployed function.
|
|
187
341
|
- The expected Linear **labels exist** (otherwise the issue is created without a label, with a warning).
|
|
188
342
|
|
|
189
343
|
## License
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Built-in inline icon keys usable on a type option. */
|
|
2
|
+
type FeedbackIconName = "bug" | "improvement" | "dot";
|
|
3
|
+
/** A selectable issue type shown in the composer's segmented control. */
|
|
4
|
+
type FeedbackTypeOption = {
|
|
5
|
+
/** Stable id sent to the server and mapped to a Linear label name. */
|
|
6
|
+
id: string;
|
|
7
|
+
/** Human label shown in the UI and used in the issue title (e.g. "Bug"). */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Swatch background color, any CSS color. */
|
|
10
|
+
color: string;
|
|
11
|
+
/** Optional built-in icon for the swatch. */
|
|
12
|
+
icon?: FeedbackIconName;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type FeedbackPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left"
|
|
16
|
+
/** Edge tabs: compact, icon-only launcher flush to the side, vertically centered. */
|
|
17
|
+
| "right" | "left";
|
|
18
|
+
type FeedbackWidgetProps = {
|
|
19
|
+
/** Endpoint that runs the server handler (default "/api/feedback"). */
|
|
20
|
+
endpoint?: string;
|
|
21
|
+
/** Brand color for the FAB, active states and focus rings (any CSS color). */
|
|
22
|
+
brandColor?: string;
|
|
23
|
+
/** Corner for the floating button (default "bottom-right"). */
|
|
24
|
+
position?: FeedbackPosition;
|
|
25
|
+
/** Selectable issue types (default Bug / Improvement). */
|
|
26
|
+
types?: FeedbackTypeOption[];
|
|
27
|
+
/** Ask for a reporter name before the first submission (default true). */
|
|
28
|
+
nameRequired?: boolean;
|
|
29
|
+
/** localStorage key for the remembered name. */
|
|
30
|
+
nameStorageKey?: string;
|
|
31
|
+
/** Label on the floating button (default "Give feedback"). */
|
|
32
|
+
fabLabel?: string;
|
|
33
|
+
/** Stacking context for the widget's layers — sugar for the `--lfb-z` CSS variable. */
|
|
34
|
+
zIndex?: number;
|
|
35
|
+
/** Extra headers sent with the submission (e.g. x-feedback-token for `headerGate`). */
|
|
36
|
+
requestHeaders?: Record<string, string>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type LinearFeedbackConfig = FeedbackWidgetProps & {
|
|
40
|
+
/** "open" (default): widget always visible. "gated": enabled via ?feedback URL param + cookie. */
|
|
41
|
+
mode?: "open" | "gated";
|
|
42
|
+
/** Shared secret sent as the x-feedback-token header — pair with `headerGate` on the server. */
|
|
43
|
+
token?: string;
|
|
44
|
+
};
|
|
45
|
+
declare function init(config?: LinearFeedbackConfig): void;
|
|
46
|
+
declare function destroy(): void;
|
|
47
|
+
|
|
48
|
+
export { type LinearFeedbackConfig, destroy, init };
|